﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the Microsoft Public License (Ms-PL).
// See accompanying file License.txt or copy at http://mews.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Mews.Net.Nntp.Cache
{
    using System;
    using System.Data.SqlServerCe;
    using System.Globalization;
    using System.IO;

    using Mews.Mime;

    /// <summary>See <see cref="IOutboundArticle"/>.</summary>
    internal sealed class OutboundArticle : IntIdObject<OutboundArticle, IOutboundArticle>, IOutboundArticle
    {
        private const int MaxFilesPerDirectory = 128;

        private readonly Host host;
        private DateTime? dateUtc;
        private string subject;
        private SendStatus sendStatus;
        private bool isDeleted;

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>See <see cref="IOutboundArticle.DateUtc"/>.</summary>
        public DateTime? DateUtc
        {
            get { return this.dateUtc; }
        }

        /// <summary>See <see cref="IOutboundArticle.Subject"/>.</summary>
        public string Subject
        {
            get { return this.subject; }
        }

        /// <summary>See <see cref="IOutboundArticle.SendStatus"/>.</summary>
        public SendStatus SendStatus
        {
            get
            {
                return this.sendStatus;
            }

            set
            {
                this.host.Database.AssertSendReceiveNotInProgress();

                if (value == SendStatus.Sent)
                {
                    throw new ArgumentException("Must not be set to SendStatus.Sent");
                }

                if (this.GetSendStatus() == SendStatus.Sent)
                {
                    throw new InvalidOperationException("Must not be modified once sent.");
                }

                this.host.Database.OutboundArticleUpdater.Update(this.Id, value);
                this.sendStatus = value;
            }
        }

        /// <summary>See <see cref="IOutboundArticle.IsDeleted"/>.</summary>
        public bool IsDeleted
        {
            get
            {
                return this.isDeleted;
            }

            set
            {
                this.host.Database.AssertSendReceiveNotInProgress();
                this.host.Database.OutboundArticleUpdater.Update(this.Id, value);
                this.isDeleted = value;
            }
        }

        /// <summary>See <see cref="IOutboundArticle.LoadMessage"/>.</summary>
        public Message LoadMessage()
        {
            try
            {
                using (var stream = File.OpenRead(GetDataFilePath(this.host.DataDirectory, this.Id)))
                {
                    return Message.Parse(stream);
                }
            }
            catch (IOException)
            {
                return new Message();
            }
        }

        /// <summary>See <see cref="IOutboundArticle.SaveMessage"/>.</summary>
        public void SaveMessage(Message message)
        {
            this.host.Database.AssertSendReceiveNotInProgress();

            if (message == null)
            {
                throw new ArgumentNullException("message");
            }

            if (this.GetSendStatus() == SendStatus.Sent)
            {
                throw new InvalidOperationException("Cannot modify sent article.");
            }

            var dataFilePath = GetDataFilePath(this.host.DataDirectory, this.Id);
            Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
            var tempFilePath = Path.ChangeExtension(dataFilePath, "temp.eml");

            using (var original = new FileStream(dataFilePath, FileMode.OpenOrCreate, FileAccess.Read, FileShare.None))
            using (var temp = new FileStream(tempFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None))
            {
                message.WriteTo(temp);
                temp.Seek(0, SeekOrigin.Begin);

                if (!AreEqual(original, temp))
                {
                    message.MessageId = "<" + Guid.NewGuid().ToString("N") + "@mews.codeplex.com>";
                }

                message.Date = DateTime.UtcNow;
                temp.Seek(0, SeekOrigin.Begin);
                message.WriteTo(temp);
                this.host.Database.OutboundArticleUpdater.Update(this.Id, message.Date, message.Subject);
                this.dateUtc = message.Date;
                this.subject = message.Subject;
            }

            File.Delete(dataFilePath);
            File.Move(tempFilePath, dataFilePath);
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>See <see cref="IOutboundArticle.Host"/>.</summary>
        IHost IOutboundArticle.Host
        {
            get { return this.Host; }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        internal OutboundArticle(Host host, SqlCeDataReader reader) : base((int)reader[OutboundArticleNames.IdColumn])
        {
            this.host = host;

            this.dateUtc = reader.GetNullableColumn<DateTime?>(OutboundArticleNames.DateUtcColumn);
            this.subject = reader.GetNullableColumn<string>(OutboundArticleNames.SubjectColumn);
            this.sendStatus = (SendStatus)reader[OutboundArticleNames.SendStatusColumn];
            this.isDeleted = (bool)reader[OutboundArticleNames.IsDeletedColumn];
        }

        internal Host Host
        {
            get { return this.host; }
        }

        internal static string GetDataFilePath(string hostDataDirectory, int id)
        {
            var subdir = Path.Combine(
                Path.Combine(hostDataDirectory, "Out"),
                (id / MaxFilesPerDirectory * MaxFilesPerDirectory).ToString(CultureInfo.InvariantCulture));
            return Path.Combine(subdir, id.ToString(CultureInfo.InvariantCulture) + ".eml");
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private SendStatus GetSendStatus()
        {
            using (var command = CommandHelper.Create(this.host.Connection, OutboundArticleNames.Table))
            using (var reader = command.ExecuteReader())
            {
                reader.SeekAndRead(DbSeekOptions.FirstEqual, this.Id);
                return (SendStatus)reader[OutboundArticleNames.SendStatusColumn];
            }
        }

        private static bool AreEqual(FileStream original, FileStream temp)
        {
            bool haveSameLength;
            int count;
            var originalBuffer = new byte[1024];
            var tempBuffer = new byte[1024];

            while ((haveSameLength =
                (count = original.Read(originalBuffer, 0, originalBuffer.Length)) ==
                temp.Read(tempBuffer, 0, tempBuffer.Length)) &&
                (count > 0))
            {
                for (int index = 0; index < count; ++index)
                {
                    if (originalBuffer[index] != tempBuffer[index])
                    {
                        return false;
                    }
                }
            }

            return haveSameLength;
        }
    }
}
